﻿using System;
using System.Collections.Generic;
using System.Linq;
using SchemaGen.Attributes;
using SchemaGen.Xsd;
using System.Reflection;

namespace SchemaGen
{
    /// <summary>
    /// Transforms code attributes into an SDocument that can be saved as an XSD file
    /// </summary>
    public class SchemaGenerator
    {
        public Type Type { get; private set; }
        public bool IsDocument { get { return DocumentAttribute != null; } }
        public XmlDocumentAttribute DocumentAttribute { get; private set; }
        public XmlComplexTypeAttribute[] ComplexTypeAttributes { get; private set; }
        public XmlSimpleTypeAttribute[] SimpleTypeAttributes { get; private set; }
        public XmlEntityBaseAttribute[] EntityAttributes { get; private set; }

        private SDocument _Document;
        /// <summary>
        /// Returns null if the type isn't marked by an XmlDocumentAttribute
        /// </summary>
        public SDocument Document 
        { 
            get 
            {
                if (_Document == null) _Document = CreateSDocument();
                return _Document;
            }
        }

        #region Constructor

        public SchemaGenerator(Type type)
        {
            Type = type;

            DocumentAttribute = (XmlDocumentAttribute)Type.GetCustomAttributes(typeof(XmlDocumentAttribute), false).FirstOrDefault();

            if (DocumentAttribute != null)
            {
                ComplexTypeAttributes =
                    GetAttributesFromType<XmlComplexTypeAttribute>(type)
                        .Concat(DocumentAttribute.IncludedTypes
                        .SelectMany(t => GetAttributesFromType<XmlComplexTypeAttribute>(t)))
                        .ToArray();

                SimpleTypeAttributes =
                    GetAttributesFromType<XmlSimpleTypeAttribute>(type)
                        .Concat(DocumentAttribute.IncludedTypes
                        .SelectMany(t => GetAttributesFromType<XmlSimpleTypeAttribute>(t)))
                        .ToArray();

                EntityAttributes =
                    GetAttributesFromType<XmlEntityBaseAttribute>(type)
                        .Concat(DocumentAttribute.IncludedTypes
                        .SelectMany(t => GetAttributesFromType<XmlEntityBaseAttribute>(t)))
                        .ToArray();
            }
        }

        #endregion

        /// <remarks>Duplicate key exceptions will be thrown if there are naming conflicts between complex elements & complex types.</remarks>
        private SDocument CreateSDocument()
        {
            if (!IsDocument) return null;
            if (EntityAttributes.Length == 0) return InitializeSDocument();

            var entityAttributeByDepth = ResolveImplicitParents(EntityAttributes)
                .GroupBy(a => a.Segments.Length)
                .OrderBy(g => g.Key)
                .ToArray();

            var document = InitializeSDocument();
            var parents = new Dictionary<string, IComplexType>();
            
            for (int i = 0; i < entityAttributeByDepth.Length; i++)
            {
                var numberOfSegments = entityAttributeByDepth[i].Key;

                // ComplexTypes are only parents on second iteration
                if (numberOfSegments == 2) foreach (var type in document.ComplexTypeDeclarations) parents.Add(type.Key.LocalName, type.Value);

                var notLastIteration = i + 1 < entityAttributeByDepth.Length;
                var nextParents = new Dictionary<string, IComplexType>();

                foreach (var entityAttribute in entityAttributeByDepth[i])
                {
                    // Check if the Clr type is an enum type & there is no simple type with that same name
                    if (entityAttribute.ClrType != null && 
                        typeof(Enum).IsAssignableFrom(entityAttribute.ClrType) &&
                        !document.SimpleTypeDeclarations.ContainsKey(entityAttribute.ClrType.Name))
                    {
                        // This enum type wasn't created, so we create it
                        var simpleType = SSimpleType.GetEnumerationRestriction(entityAttribute.ClrType);
                        simpleType.Name = entityAttribute.ClrType.Name;
                        document.SimpleTypeDeclarations.Add(simpleType.Name, simpleType);
                        if (document.ComplexTypeDeclarations.ContainsKey(simpleType.Name))
                            throw new SchemaException("There is a name conflict between a simple type and complex type. Both are named " + simpleType.Name);
                    }

                    var entity = ProcessEntity(entityAttribute);
                    if (entityAttribute.IsComplexElement) nextParents.Add(entityAttribute.Name, (SComplexElement)entity);

                    // Elements are added to Root if they have exactly one segment
                    if (numberOfSegments > 1)
                        parents[entityAttribute.ParentFullName].Add(entity);
                    else
                        document.RootElement.Add(entity);
                }

                parents = nextParents;
            }

            return document;
        }

        private SDocument InitializeSDocument()
        {
            var result = new SDocument(DocumentAttribute.RootElementName, DocumentAttribute.Namespace);
            result.FilePath = DocumentAttribute.FileName;
            result.RootElement.ComplexType.SequenceIndicator = DocumentAttribute.SequenceIndicator;
            foreach (var c in ComplexTypeAttributes)
            {
                var complex = new SComplexType()
                {
                    Name = c.Name,
                    SequenceIndicator = c.SequenceIndicator,
                    Extends = c.Extends
                };
                result.ComplexTypeDeclarations.Add(c.Name, complex);
            }
            foreach (var s in SimpleTypeAttributes) result.SimpleTypeDeclarations.Add(s.Name, s.Restriction);

            return result;
        }

        /// <remarks>Duplicate key exceptions will be thrown if there are duplicate element names.</remarks>
        private XmlEntityBaseAttribute[] ResolveImplicitParents(XmlEntityBaseAttribute[] entityAttributes)
        {
            var entityAttributesList = EntityAttributes
                .OrderBy(e => e.SequenceOrder)
                .ToList();

            var entityLookup = new SortedDictionary<string, XmlEntityBaseAttribute>(entityAttributesList.ToDictionary(a => a.Name));
            var complexTypeLookup = new SortedList<string, XmlComplexTypeAttribute>(ComplexTypeAttributes.ToDictionary(c => c.Name));

            // Find implied parents and insert them above their first children
            for (int i = 0; i < entityAttributesList.Count; i++)
            {
                if (entityAttributesList[i].ParentFullName != null)
                {
                    XmlEntityBaseAttribute declaredParent;
                    if (entityLookup.TryGetValue(entityAttributesList[i].ParentFullName, out declaredParent))
                    {
                        declaredParent.IsComplexElement = true;
                        if (declaredParent.TypeName != null)
                            throw new SchemaException("An element may not have both a type and child elements. Type: " + declaredParent.Name);
                    }
                    else if (!complexTypeLookup.ContainsKey(entityAttributesList[i].ParentFullName))
                    {
                        var impliedParent = new XmlElementAttribute(entityAttributesList[i].ParentFullName)
                        {
                            IsComplexElement = true
                        };
                        entityLookup.Add(impliedParent.Name, impliedParent);
                        // Iteration gets advanced here to skip inserted parent
                        entityAttributesList.Insert(i++, impliedParent);
                    }
                }
            }

            return entityAttributesList.ToArray();
        }

        private SEntityBase ProcessEntity(XmlEntityBaseAttribute attribute)
        {
            if (attribute.GetType().Equals(typeof(XmlElementAttribute)))
            {
                return attribute.IsComplexElement ?
                    (SEntityBase)ProcessComplexElement((XmlElementAttribute)attribute):
                    (SEntityBase)ProcessSimpleElement((XmlElementAttribute)attribute);
            }
            else // Is an attribute
            {
                return ProcessAttribute((XmlAttributeAttribute)attribute);
            }
        }

        private SSimpleElement ProcessSimpleElement(XmlElementAttribute attribute)
        {
            return new SSimpleElement(attribute.LocalName)
            {
                // Common
                Type = attribute.TypeName,
                Documentation = attribute.Documentation,
                DefaultValue = attribute.DefaultValue,
                IsFixed = attribute.IsFixed,

                // Simple Element
                MinOccurs = attribute.MinOccurs,
                MaxOccurs = attribute.MaxOccurs,

                // Restriction property (in-line type def) isn't assignable via the attribute API
            };
        }

        private SComplexElement ProcessComplexElement(XmlElementAttribute attribute)
        {
            if (attribute.NamedType != null) throw new InvalidOperationException("And element cannot have both a Type and Children.");

            var result = new SComplexElement(attribute.LocalName)
            {
                // Common
                Type = attribute.TypeName,
                Documentation = attribute.Documentation,
                DefaultValue = attribute.DefaultValue,
                IsFixed = attribute.IsFixed,

                // Complex Element
                MinOccurs = attribute.MinOccurs,
                MaxOccurs = attribute.MaxOccurs,
            };

            if (attribute.SequenceIndicator != SequenceIndicator.All)
                result.ComplexType.SequenceIndicator = attribute.SequenceIndicator;

            return result;
        }

        private SAttribute ProcessAttribute(XmlAttributeAttribute attribute)
        {
            return new SAttribute(attribute.LocalName)
            {
                // Common
                Type = attribute.TypeName,
                Documentation = attribute.Documentation,
                DefaultValue = attribute.DefaultValue,
                IsFixed = attribute.IsFixed,

                // Attribute
                IsRequired = attribute.IsRequired,

                // Restriction property (in-line type def) isn't assignable via the attribute API
            };
        }

        private IEnumerable<T> GetAttributesFromType<T>(Type type, bool inherit = true)
            where T : Attribute
        {
            // ---------------------------
            // -- WARNING: For some reason, Type.GetMembers will NOT get privately declared members, even with BindingFlags.NonPublic set.
            //            All attributes must be set on public members, or they will not be added to the schema.
            //  ---------------------------

            foreach (var attribute in type.GetCustomAttributes(typeof(T), inherit)) yield return (T)attribute;

            foreach (var member in type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static))
            {
                foreach (var attribute in member.GetCustomAttributes(typeof(T), inherit)) yield return (T)attribute;
            }
        }
    }
}
